﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;

namespace Chat
{
    public class Logger
    {
        public enum LogLevel
        {
            Fatal = 0,
            Error = 1,
            Debug = 2,
            Warning = 3,
            Info = 4
        }

        private LogLevel m_logLevel;
        private static Logger m_logger = new Logger();

        // Private API

        private Logger() { m_logLevel = LogLevel.Info; }

        private void PrivateFatal(String message)
        {
            if (m_logLevel >= LogLevel.Fatal)
            {
                Console.WriteLine("FATAL: " + message);
            }
        }

        private void PrivateError(String message)
        {
            if (m_logLevel >= LogLevel.Error)
            {
                Console.WriteLine("ERROR: " + message);
            }
        }

        private void PrivateDebug(String message)
        {
            if (m_logLevel >= LogLevel.Debug)
            {
                Console.WriteLine("DEBUG: " + message);
            }
        }

        private void PrivateWarning(String message)
        {
            if (m_logLevel >= LogLevel.Warning)
            {
                Console.WriteLine("WARNING: " + message);
            }
        }

        private void PrivateInfo(String message)
        {
            if (m_logLevel >= LogLevel.Info)
            {
                Console.WriteLine("INFO: " + message);
            }
        }

        // Public API

        public static void Fatal(String message)
        {
            m_logger.PrivateFatal(message);
        }

        public static void Error(String message)
        {
            m_logger.PrivateError(message);
        }

        public static void Debug(String message)
        {
            m_logger.PrivateDebug(message);
        }

        public static void Warning(String message)
        {
            m_logger.PrivateWarning(message);
        }

        public static void Info(String message)
        {
            m_logger.PrivateInfo(message);
        }

    }

    public static class DataPacket
    {
        // Private data types

        private struct ReceiveData
        {
            public AsyncCallback callback;
            public Socket socket;
            public byte[] buffer;
        }

        // Public API

        public static bool Send(Socket socket, String data)
        {
            byte[] dataBytes = Encoding.UTF8.GetBytes(data);

            int size = dataBytes.Length;

            socket.Send(BitConverter.GetBytes(size));

            return (socket.Send(dataBytes) == dataBytes.Length);
        }

        public static bool SendAsync(Socket socket, String data)
        {
            byte[] dataBytes = Encoding.UTF8.GetBytes(data);

            int size = dataBytes.Length;

            SocketAsyncEventArgs sizeArg = new SocketAsyncEventArgs();
            sizeArg.SetBuffer(BitConverter.GetBytes(size), 0, sizeof(int));
            socket.SendAsync(sizeArg);

            SocketAsyncEventArgs dataArg = new SocketAsyncEventArgs();
            dataArg.SetBuffer(dataBytes, 0, dataBytes.Length);

            return (socket.SendAsync(dataArg));
        }

        public static String Receive(Socket socket)
        {
            byte[] dataSize = new byte[4];

            socket.Receive(dataSize);

            int size =  BitConverter.ToInt32(dataSize, 0);

            byte[] data = new byte[size];

            socket.Receive(data);

            return Encoding.UTF8.GetString(data);
        }

        public static void ReceiveAsync(AsyncCallback callback, Socket socket) // In IAsyncResult you will find received byte[]
        {
            ReceiveData data = new ReceiveData();
            
            data.callback = callback;
            data.socket = socket;
            data.buffer =  new byte[4];

            socket.BeginReceive(
                data.buffer,
                0,
                data.buffer.Length,
                SocketFlags.None,
                new AsyncCallback(ReceiveCallback),
                data);
        }

        private static void ReceiveCallback(IAsyncResult result)
        {
            ReceiveData data = (ReceiveData) result.AsyncState;

            int receivingDataSize = BitConverter.ToInt32(data.buffer, 0);

            byte[] receivingData = new byte[receivingDataSize];

            data.socket.BeginReceive(
                receivingData,
                0,
                receivingDataSize,
                SocketFlags.None,
                data.callback,
                receivingData);
        }
    }

    public interface IMessageDelegate
    {
        bool GotMessage(String senderId, String receiverId, String message);
        void GotDisconnectRequest(Connection connection);
        void GotDeliveryStatus(Connection connection, ChatData.DeliveryStatus status);
    }

    public static class ChatData
    {
        // Private data

        private static readonly String[] kDeliveryStatusStrings = 
        {
            "OK",
            "UserNotFound"
        };

        private static readonly String[] kActionStrings =
        {
            "IncomingMessage",
            "DeliveryStatus",
            "Disconnect"
        };

        private static readonly uint[] kDataMaxSizes =
        {
            32, // Max nickname length
            32, // Max nickname length
            4096, // Max message length
            32, // Max room id length
            64
        };

        // Public data

        public enum Key
        {
            SenderId = 0,
            ReceiverId = 1,
            Message = 2,
            RoomId,
            Action,
            KeysCount
        };

        public enum ConnectionAction
        {
            IncomingMessage,
            DeliveryStatus,
            Disconnect,
            ActionsCount
        };

        public enum DeliveryStatus
        {
            OK,
            UserNotFound,
            DeliveryStatusCount
        }

        // Private API

        private static bool IsValidId(String id)
        {
            for (int i = 0; i < id.Length; i++)
            {
                // We accept only letters from A to Z in upper and lower case and _ sign
                if ((id[i] < 'a' && id[i] > 'z')
                    && (id[i] < 'A' && id[i] > 'Z')
                    && (id[i] != '_'))
                {
                    return false;
                }
            }

            return true;
        }

        private static bool IsValidMessage(String message)
        {
            return true;
        }

        private static bool IsValidRoomId(String roomId)
        {
            if (roomId[0] != '@')
            {
                return false;
            }

            return IsValidId(roomId.Substring(1));
        }

        private static bool IsValidAction(String action)
        {
            return StringToAction(action) != ConnectionAction.ActionsCount;
        }

        // Public API

        public static bool IsValidData(Key key, String data)
        {
            if (data == null)
            {
                return false;
            }

            if (key >= Key.KeysCount)
            {
                return false;
            }

            if (data.Length == 0)
            {
                return false;
            }

            if (data.Length > kDataMaxSizes[(int)key])
            {
                return false;
            }

            switch (key)
            {
                case Key.SenderId:
                case Key.ReceiverId:
                    return IsValidId(data);
                case Key.Message:
                    return IsValidMessage(data);
                case Key.RoomId:
                    return IsValidRoomId(data);
                case Key.Action:
                    return IsValidAction(data);
                default:
                    return false;
            }
        }

        public static uint MaxLengthForKey(Key key)
        {
            return kDataMaxSizes[(int) key];
        }

        public static ConnectionAction StringToAction(String action)
        {
            ConnectionAction result = 0;
            while (result < ConnectionAction.ActionsCount && !(action.Equals(kActionStrings[(int) result])))
            {
                result++;
            }

            return result;
        }

        public static String ActionToString(ConnectionAction action)
        {
            if (action < ConnectionAction.ActionsCount)
            {
                return kActionStrings[(int)action];
            }

            return String.Empty;
        }

        public static DeliveryStatus StringToDeliveryStatus(String status)
        {
            DeliveryStatus result = 0;
            while (result < DeliveryStatus.DeliveryStatusCount && !(status.Equals(kDeliveryStatusStrings[(int) result])))
            {
                result++;
            }

            return result;
        }

        public static String DeliveryStatusToString(DeliveryStatus status)
        {
            if (status < DeliveryStatus.DeliveryStatusCount)
            {
                return kDeliveryStatusStrings[(int) status];
            }

            return String.Empty;
        }
    }

    public class Connection : IDisposable
    {
        private int kInfiniteTimeout = -1;
        private int kDefaultTimeout = 10000; // 10 sec
        private System.Threading.Semaphore m_semaphore = new System.Threading.Semaphore(1, 1);

        public enum SendingStatus
        {
            OK = 0,
            InvalidSenderId,
            InvalidReceiverId,
            InvalidMessage,
            FailedToSendAction,
            FailedToSendSenderId,
            FailedToSendReceiverId,
            FailedToSendMessage,
            FatalNotConnected
        };

        private enum ConnectionState
        {
            Idle = 0,
            IncomingMessageAction,
            DisconnectAction,
            SenderIdReceived,
            ReceiverIdReceived,
            MessageReceived,
            WaitingForDeliveryStatus, // State after SendMessage
            ReceivingDeliveryStatus,
            Timeout
        };

        // Constructor

        public Connection(Socket socket, String clientId, IMessageDelegate messageDelegate)
        {
            m_socket = socket;
            m_clientId = clientId;
            m_messageDelegate = messageDelegate;

            m_state = ConnectionState.Idle;

            ProcessCurrentState();
        }

        // Destructor

        ~Connection()
        {
            Disconnect();
            Dispose();
        }

        // IDisposable implementation

        public void Dispose()
        {
            m_socket.Dispose();
        }

        // Overrides

        public override String ToString()
        {
            return m_clientId;
        }

        public override bool Equals(object obj)
        {
            if (obj.GetType() == this.GetType())
            {
                return ((Connection) obj).m_clientId.Equals(m_clientId);
            }

            return false;
        }

        // Public API

        public SendingStatus SendMessage(String senderId, String receiverId, String message)
        {
            System.Diagnostics.Debug.Assert(!(senderId.Equals(receiverId)));

            m_semaphore.WaitOne();

            while (m_state != ConnectionState.Idle)
            {
                m_semaphore.Release();
                System.Threading.Thread.Sleep(10);
                m_semaphore.WaitOne();
            }

            if (!ChatData.IsValidData(ChatData.Key.SenderId, senderId))
            {
                Logger.Error(String.Format("Invalid senderId, user {0} ip {1}", GetUserId(), GetRemoteIp()));
                m_semaphore.Release();
                return SendingStatus.InvalidSenderId;
            }

            if (!ChatData.IsValidData(ChatData.Key.ReceiverId, receiverId))
            {
                Logger.Error(String.Format("Invalid receiverId, user {0} ip {1}", GetUserId(), GetRemoteIp()));
                m_semaphore.Release();
                return SendingStatus.InvalidReceiverId;
            }

            if (!ChatData.IsValidData(ChatData.Key.Message, message))
            {
                Logger.Error(String.Format("Invalid message, user {0} ip {1}", GetUserId(), GetRemoteIp()));
                m_semaphore.Release();
                return SendingStatus.InvalidMessage;
            }

            String actionKey = ChatData.ActionToString(ChatData.ConnectionAction.IncomingMessage);
            if (!DataPacket.SendAsync(m_socket, actionKey))
            {
                m_semaphore.Release();
                return SendingStatus.FailedToSendAction;
            }

            if (!DataPacket.SendAsync(m_socket, senderId))
            {
                Logger.Fatal(String.Format("Failed to send senderId, user {0} ip {1}", GetUserId(), GetRemoteIp()));
                m_semaphore.Release();
                return SendingStatus.FailedToSendSenderId;
            }

            if (!DataPacket.SendAsync(m_socket, receiverId))
            {
                Logger.Fatal(String.Format("Failed to send receiverId, user {0} ip {1}", GetUserId(), GetRemoteIp()));
                m_semaphore.Release();
                return SendingStatus.FailedToSendReceiverId;
            }

            m_state = ConnectionState.WaitingForDeliveryStatus;

            if (!DataPacket.SendAsync(m_socket, message))
            {
                Logger.Fatal(String.Format("Failed to send message, user {0} ip {1}", GetUserId(), GetRemoteIp()));
                m_state = ConnectionState.Idle;
                m_semaphore.Release();
                return SendingStatus.FailedToSendMessage;
            }

            Logger.Info(String.Format("Waiting for delivery status from {0}", receiverId));
            m_semaphore.Release();

            return SendingStatus.OK;
        }

        public String GetUserId()
        {
            return m_clientId;
        }

        public bool SetUserId(String id)
        {
            if (ChatData.IsValidData(ChatData.Key.SenderId, id))
            {
                m_clientId = id;
                return true;
            }

            return false;
        }

        public void Disconnect()
        {
            if (m_socket.Connected)
            {
                DataPacket.SendAsync(m_socket, ChatData.ActionToString(ChatData.ConnectionAction.Disconnect));
                m_socket.Disconnect(false);
            }
        }

        public String GetRemoteIp()
        {
            return m_socket.RemoteEndPoint.ToString();
        }

        public String GetLocalIp()
        {
            return m_socket.LocalEndPoint.ToString();
        }

        // Private API

        private void GotIncomingAction(IAsyncResult result)
        {
            m_semaphore.WaitOne();

            String action = Encoding.UTF8.GetString((byte[]) result.AsyncState);

            if (action.Equals(""))
            {
                Logger.Fatal(String.Format("Received empty string from user {0} ip {1}, disconnecting", GetUserId(), GetRemoteIp()));
                if (m_messageDelegate != null)
                {
                    m_messageDelegate.GotDisconnectRequest(this);
                }
                m_semaphore.Release();

                return;
            }

            switch (ChatData.StringToAction(action))
            {
                case ChatData.ConnectionAction.IncomingMessage:
                    m_state = ConnectionState.IncomingMessageAction;
                    m_socket.ReceiveTimeout = kDefaultTimeout;
                    DataPacket.ReceiveAsync(new AsyncCallback(GotIncomingSenderId), m_socket);
                    Logger.Debug(String.Format("Idle -> IncomingMessageAction from user {0} ip {1}", GetUserId(), GetRemoteIp()));
                    return;
                case ChatData.ConnectionAction.Disconnect:
                    m_state = ConnectionState.DisconnectAction;
                    Logger.Debug(String.Format("Idle -> DisconnectAction from user {0} ip {1}", GetUserId(), GetRemoteIp()));
                    break;
                case ChatData.ConnectionAction.DeliveryStatus:
                    if (m_state == ConnectionState.WaitingForDeliveryStatus)
                    {
                        DataPacket.ReceiveAsync(new AsyncCallback(GotDeliveryStatus), m_socket);
                        Logger.Debug(String.Format("Idle -> ReceivingDeliveryStatus from user {0} ip ", GetUserId(), GetRemoteIp()));
                        return;
                    }
                    else // m_state == ConnectionState.Idle
                    {
                        Logger.Error(String.Format("Received delivery status in idle state from user {0} ip {1}", GetUserId(), GetRemoteIp()));
                        m_semaphore.Release();
                    }
                    break;
                default:
                    Logger.Error(String.Format("Invalid action from address user {0} ip {1}", GetUserId(), GetRemoteIp()));
                    m_semaphore.Release();
                    break;
            }

            ProcessCurrentState();
        }

        private void GotIncomingSenderId(IAsyncResult result)
        {
            try
            {
                m_senderId = String.Empty;
                m_receiverId = String.Empty;
                m_message = String.Empty;

                m_senderId = Encoding.UTF8.GetString((byte[]) result.AsyncState);

                if (m_senderId.Length != 0)
                {
                    m_state = ConnectionState.SenderIdReceived;
                    Logger.Debug(String.Format("IncomingMessageAction -> SenderIdReceived from user {0} ip {1}", GetUserId(), GetRemoteIp()));
                }
                else
                {
                    m_state = ConnectionState.Idle;
                    Logger.Debug(String.Format("IncomingMessageAction -> Idle, received empty senderId from user {0} ip {1}", GetUserId(), GetRemoteIp()));
                    m_semaphore.Release();
                    ProcessCurrentState();
                    return;
                }

                DataPacket.ReceiveAsync(new AsyncCallback(GotIncomingReceiverId), m_socket);
            }
            catch (SocketException)
            {
                m_state = ConnectionState.Timeout;
                m_semaphore.Release();
                ProcessCurrentState();
            }
        }

        private void GotIncomingReceiverId(IAsyncResult result)
        {
            try
            {
                m_receiverId = Encoding.UTF8.GetString((byte[]) result.AsyncState);

                if (m_receiverId.Length != 0)
                {
                    m_state = ConnectionState.ReceiverIdReceived;
                    Logger.Debug(String.Format("IncomingMessageAction -> ReceiverIdReceived from user {0} ip {1}", GetUserId(), GetRemoteIp()));
                }
                else
                {
                    m_state = ConnectionState.Idle;
                    Logger.Debug(String.Format("IncomingMessageAction -> Idle, received empty receiverId from user {0} ip {1}", GetUserId(), GetRemoteIp()));
                    m_semaphore.Release();
                    ProcessCurrentState();
                    return;
                }

                DataPacket.ReceiveAsync(new AsyncCallback(GotIncomingMessage), m_socket);
            }
            catch (SocketException)
            {
                m_state = ConnectionState.Timeout;
                m_semaphore.Release();
                ProcessCurrentState();
            }
        }

        private void GotIncomingMessage(IAsyncResult result)
        {
            try
            {
                m_message = Encoding.UTF8.GetString((byte[])result.AsyncState);

                if (m_message.Length != 0)
                {
                    m_state = ConnectionState.MessageReceived;
                    Logger.Debug(String.Format("IncomingMessageAction -> MessageReceived from user {0} ip {1}", GetUserId(), GetRemoteIp()));
                }
                else
                {
                    m_state = ConnectionState.Idle;
                    Logger.Debug(String.Format("IncomingMessageAction -> Idle, received empty message from user {0} ip {1}", GetUserId(), GetRemoteIp()));
                    m_semaphore.Release();
                    ProcessCurrentState();
                    return;
                }

                System.Diagnostics.Debug.Assert(ChatData.IsValidData(ChatData.Key.SenderId, m_senderId));
                System.Diagnostics.Debug.Assert(ChatData.IsValidData(ChatData.Key.ReceiverId, m_receiverId));
                System.Diagnostics.Debug.Assert(ChatData.IsValidData(ChatData.Key.Message, m_message));

                DataPacket.SendAsync(m_socket, ChatData.ActionToString(ChatData.ConnectionAction.DeliveryStatus));
                if (m_messageDelegate != null)
                {
                    if (m_messageDelegate.GotMessage(m_senderId, m_receiverId, m_message))
                    {
                        DataPacket.SendAsync(m_socket, ChatData.DeliveryStatusToString(ChatData.DeliveryStatus.OK));
                        Logger.Debug(String.Format("Sent status OK to user {0} ip {1}", GetUserId(), GetRemoteIp()));
                    }
                    else
                    {
                        DataPacket.SendAsync(m_socket, ChatData.DeliveryStatusToString(ChatData.DeliveryStatus.UserNotFound));
                        Logger.Debug(String.Format("Sent status UserNotFound to user {0} ip {1}", GetUserId(), GetRemoteIp()));
                    }
                }
                else
                {
                    DataPacket.SendAsync(m_socket, ChatData.DeliveryStatusToString(ChatData.DeliveryStatus.UserNotFound));
                    Logger.Debug(String.Format("Sent status UserNotFound to user {0} ip {1}", GetUserId(), GetRemoteIp()));
                }

                Logger.Debug(String.Format("MessageReceived -> Idle from user {0} ip {1}", GetUserId(), GetRemoteIp()));
                m_state = ConnectionState.Idle;
                m_semaphore.Release();
            }
            catch (SocketException)
            {
                m_state = ConnectionState.Timeout;
                m_semaphore.Release();
            }

            ProcessCurrentState();
        }

        private void GotDeliveryStatus(IAsyncResult result)
        {
            try
            {
                String status = Encoding.UTF8.GetString((byte[]) result.AsyncState);

                Logger.Debug(String.Format("Got delivery status {0} from user {1} ip {2}", status, GetUserId(), GetRemoteIp()));

                if (m_messageDelegate != null)
                {
                    m_messageDelegate.GotDeliveryStatus(this, ChatData.StringToDeliveryStatus(status));
                }

                Logger.Debug(String.Format("ReceivingDeliveryStatus -> Idle from user {0} ip {1}", GetUserId(), GetRemoteIp()));
                m_state = ConnectionState.Idle;
            }
            catch (SocketException)
            {
                m_state = ConnectionState.Timeout;
            }
            
            m_semaphore.Release();
            ProcessCurrentState();
        }

        private void ProcessCurrentState()
        {
            switch (m_state)
            {
                case ConnectionState.Idle:
                    m_socket.ReceiveTimeout = kInfiniteTimeout;
                    DataPacket.ReceiveAsync(new AsyncCallback(GotIncomingAction), m_socket);
                    break;

                case ConnectionState.DisconnectAction:
                    m_state = ConnectionState.Idle;
                    m_messageDelegate.GotDisconnectRequest(this);
                    break;

                case ConnectionState.Timeout:
                    if (m_messageDelegate != null)
                    {
                        m_messageDelegate.GotDisconnectRequest(this);
                    }
                    else
                    {
                        Disconnect();
                    }
                    break;
                default:
                    System.Diagnostics.Debug.Assert(false);
                    break;
            }
        }

        // Private Data

        private Socket m_socket;
        private String m_clientId;

        private ConnectionState m_state;

        private IMessageDelegate m_messageDelegate;


        private String m_senderId;
        private String m_receiverId;
        private String m_message;
    }
}
